/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 2007-2009  Intel Corporation. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <unistd.h>
#include <sys/wait.h>
#include <glib/gstdio.h>

#include <errno.h>
#include <string.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <net/route.h>

#define CONNMAN_API_SUBJECT_TO_CHANGE
#include <connman/plugin.h>
#include <connman/element.h>
#include <connman/ipconfig.h>
#include <connman/inet.h>
#include <connman/dbus.h>
#include <connman/log.h>
#include <connman/profile.h>
#include <connman/resolver.h>

#define	_DBG_DHCPCD(fmt, arg...)	DBG(DBG_DHCLIENT, fmt, ## arg)

#define DHCPCD_INTF "org.chromium.dhcpcd"
#define DHCPCD_PATH "/org/chromium/dhcpcd"

#define	MAXDNS		10		/* max DNS servers per lease */
#define	MAXDOMAINS	10		/* max DNS search domains per lease */
#define	ADDR_SIZE	sizeof("255.255.255.255")	/* NB: includes \0 */

/* TODO(sleffler) rewrite using task support */
struct dhcpcd_task {
	GPid pid;
	char *dbus_address;
	gboolean killed;
	struct connman_ipconfig *ipconfig;
};

static GSList *task_list = NULL;

static DBusConnection *connection;

static int dhcpcd_renew(struct connman_ipconfig *ipconfig);

static struct dhcpcd_task *find_task_by_pid(GPid pid)
{
	GSList *list;

	for (list = task_list; list; list = list->next) {
		struct dhcpcd_task *task = list->data;
		if (task->pid == pid)
			return task;
	}
	return NULL;
}

static struct dhcpcd_task *find_task_by_ipconfig(
    const struct connman_ipconfig *ipconfig)
{
	GSList *list;

	for (list = task_list; list; list = list->next) {
		struct dhcpcd_task *task = list->data;
		if (task->ipconfig == ipconfig && task->killed == FALSE)
			return task;
	}
	return NULL;
}

static void kill_task(struct dhcpcd_task *task)
{
	const char *ifname = connman_ipconfig_get_ifname(task->ipconfig);

	_DBG_DHCPCD("task %p name %s pid %d", task, ifname, task->pid);

	if (task->killed == FALSE && task->pid > 0) {
		if (kill(task->pid, SIGTERM) < 0 && errno != ESRCH) {
			connman_error("dhcpcd(%s): kill pid %d: %s",
			    ifname, task->pid, strerror(errno));
			/* TODO(sleffler) send SIGKILL? */
		}
		task->killed = TRUE;
	}
}

static void unlink_task(struct dhcpcd_task *task)
{
	const char *ifname = connman_ipconfig_get_ifname(task->ipconfig);
	gchar *pathname;

	_DBG_DHCPCD("task %p name %s pid %d", task, ifname, task->pid);

	pathname = g_strdup_printf("/var/run/dhcpcd/dhcpcd-%s.pid", ifname);
	g_unlink(pathname);
	g_free(pathname);

	pathname = g_strdup_printf("/var/lib/dhcpcd/dhcpcd-%s.lease", ifname);
	g_unlink(pathname);
	g_free(pathname);
}

static int start_dhcpcd(struct dhcpcd_task *task);

static void task_died(GPid pid, gint status, gpointer data)
{
	struct dhcpcd_task *task = data;

	if (WIFEXITED(status))
		_DBG_DHCPCD("exit status %d for %s", WEXITSTATUS(status),
		    connman_ipconfig_get_ifname(task->ipconfig));
	else
		_DBG_DHCPCD("signal %d killed %s", WTERMSIG(status),
		    connman_ipconfig_get_ifname(task->ipconfig));

	g_spawn_close_pid(pid);
	task->pid = 0;

	task_list = g_slist_remove(task_list, task);

	unlink_task(task);
	g_free(task->dbus_address);

	g_free(task);
}

static void task_setup(gpointer data)
{
	struct dhcpcd_task *task = data;

	_DBG_DHCPCD("task %p name %s", task,
	    connman_ipconfig_get_ifname(task->ipconfig));

	task->killed = FALSE;
}

static int start_dhcpcd(struct dhcpcd_task *task)
{
	const char *ifname = connman_ipconfig_get_ifname(task->ipconfig);
	char *argv[5], *envp[1];

	argv[0] = DHCPCD;
	argv[1] = "-B";			/* foreground */
	argv[2] = (char *) ifname;	/* TODO(sleffler) __DECONST */
	if (connman_profile_get_arpgateway()) {
		argv[3] = "-R";         /* ARP for default gateway */
		argv[4] = NULL;
	} else
		argv[3] = NULL;

	envp[0] = NULL;

	if (g_spawn_async(NULL, argv, envp, G_SPAWN_DO_NOT_REAP_CHILD,
				task_setup, task, &task->pid, NULL) == FALSE) {
		connman_error("%s: failed to spawn %s for %s",
		    __func__, DHCPCD, ifname);
		return -1;
	}

	task_list = g_slist_append(task_list, task);

	g_child_watch_add(task->pid, task_died, task);

	_DBG_DHCPCD("spawn %s with pid %d", DHCPCD, task->pid);

	return 0;
}

static int dhcpcd_request(struct connman_ipconfig *ipconfig)
{
	struct dhcpcd_task *task;
	const char *ifname;

	ifname = connman_ipconfig_get_ifname(ipconfig);
	_DBG_DHCPCD("request %s ipconfig %p", ifname, ipconfig);

	task = find_task_by_ipconfig(ipconfig);
	if (task != NULL) {
		if (task->dbus_address != NULL) {
			/*
			 * Just ask the existing process to renew it's current
			 * lease.  If that fails it will fallback quicky to
			 * requesting a new lease.
			 */
			return dhcpcd_renew(ipconfig);
		} else {
			/*
			 * We are in an uncomfortable situation where
			 * we have a dhcpcd task running, but we don't
			 * (yet) have a dbus handle for it.  Kill this
			 * (since we do know its PID), and start a new
			 * one.
			 */
			kill_task(task);
		}
	}

	if (ifname == NULL)
		return -EINVAL;

	task = g_try_new0(struct dhcpcd_task, 1);
	if (task == NULL)
		return -ENOMEM;
	task->ipconfig = ipconfig;

	return start_dhcpcd(task);
}

static int dhcpcd_renew(struct connman_ipconfig *ipconfig)
{
	DBusMessage *message, *reply;
	DBusError error;
	const char *ifname = connman_ipconfig_get_ifname(ipconfig);
	struct dhcpcd_task *task;

	_DBG_DHCPCD("renew %s", ifname);

	if (ifname == NULL)
		return -EINVAL;

	task = find_task_by_ipconfig(ipconfig);
	if (task == NULL)
		return -EINVAL;		/* XXX */

	if (task->dbus_address == NULL)
		return -EINVAL;

	message = dbus_message_new_method_call(task->dbus_address, DHCPCD_PATH,
					       DHCPCD_INTF, "Rebind");
	if (message == NULL)
		return -ENOMEM;

	dbus_error_init(&error);

	/* TODO(njw): If dhcpcd changes to synchronously perform the
	   operation before returning, this will hang until it's done,
	   so it might be better to use the nonblocking call here. */
	reply = dbus_connection_send_with_reply_and_block(connection, message,
							  -1, &error);
	if (reply == NULL) {
		if (dbus_error_is_set(&error) == TRUE) {
			connman_error("%s: %s", __func__, error.message);
			dbus_error_free(&error);
		} else
			connman_error("%s: failed", __func__);
		dbus_message_unref(message);
		return -EIO;
	}

	dbus_message_unref(message);

	return 0;
}

static int dhcpcd_release(struct connman_ipconfig *ipconfig)
{
	DBusMessage *message, *reply;
	DBusError error;
	const char *ifname = connman_ipconfig_get_ifname(ipconfig);
	struct dhcpcd_task *task;

	_DBG_DHCPCD("ipconfig %p ifname %s", ipconfig, ifname);

	task = find_task_by_ipconfig(ipconfig);
	if (task == NULL)
		return -EINVAL;

	if (task->dbus_address == NULL) {
		kill_task(task);
		return -EINVAL;
	}

	message = dbus_message_new_method_call(task->dbus_address, DHCPCD_PATH,
					       DHCPCD_INTF, "Release");
	if (message == NULL)
		return -ENOMEM;

	dbus_error_init(&error);

	/* TODO(njw): See dhcpcd_renew() */
	reply = dbus_connection_send_with_reply_and_block(connection, message,
							  -1, &error);
	if (reply == NULL) {
		if (dbus_error_is_set(&error) == TRUE) {
			connman_error("%s: %s", __func__, error.message);
			dbus_error_free(&error);
		} else
			connman_error("%s: failed", __func__);
		dbus_message_unref(message);
		return -EIO;
	}

	dbus_message_unref(message);

	kill_task(task);

	return 0;
}

static struct connman_ipconfig_driver dhcpcd_driver = {
	.name		= "dhcpcd",
	.type		= CONNMAN_IPCONFIG_TYPE_DHCP,
	.priority	= CONNMAN_IPCONFIG_PRIORITY_DEFAULT,
	.request	= dhcpcd_request,
	.release	= dhcpcd_release,
	.renew		= dhcpcd_renew,
};

static DBusHandlerResult bad_type(const char *key, int type_expect, int type_got)
{
	connman_error("dhcpcd_filter: bad d-bus type for key %s: "
	    "got 0x%x, expected 0x%x", key, type_got, type_expect);
	return DBUS_HANDLER_RESULT_HANDLED;
}

static DBusHandlerResult bad_array_size(const char *key, int count, int max)
{
	connman_error("dhcpcd_filter: bad d-bus array size for key %s: "
	    "got %d elements, max %d", key, count, max);
	return DBUS_HANDLER_RESULT_HANDLED;
}

/*
 * D-Bus helper functions for parsing basic values enclosed in a variant.
 */

static int _get_basic(DBusMessageIter *iter, const char *key,
    int basic_type, void *value)
{
	DBusMessageIter entry;
	int type;

	dbus_message_iter_recurse(iter, &entry);
	type = dbus_message_iter_get_arg_type(&entry);
	if (type != basic_type) {
		(void) bad_type(key, basic_type, type);
		return FALSE;
	} else {
		dbus_message_iter_get_basic(&entry, value);
		return TRUE;
	}
}
static int get_byte(DBusMessageIter *iter, const char *key, uint8_t *value)
{
	int status = _get_basic(iter, key, DBUS_TYPE_BYTE, value);
	_DBG_DHCPCD("%s = 0x%x (byte)", key, *value);
	return status;
}
static int get_uint16(DBusMessageIter *iter, const char *key, uint16_t *value)
{
	int status = _get_basic(iter, key, DBUS_TYPE_UINT16, value);
	_DBG_DHCPCD("%s = 0x%x (uint16)", key, *value);
	return status;
}
#if 0
static int get_uint32(DBusMessageIter *iter, const char *key, uint32_t *value)
{
       int status = _get_basic(iter, key, DBUS_TYPE_UINT32, value);
       _DBG_DHCPCD("%s = 0x%x (uint32)", key, *value);
       return status;
}
#endif
static int get_string(DBusMessageIter *iter, const char *key, char **value)
{
	int status = _get_basic(iter, key, DBUS_TYPE_STRING, value);
	_DBG_DHCPCD("%s = %s (string)", key, *value);
	return status;
}

/*
 * Convert an IPv4 address in network byte order to dot'd quad string.
 * Return 1 if successful, 0 if there is a problem in the conversion.
 */
static int _cvt_ipv4(char str[ADDR_SIZE], uint32_t addr)
{
	return (inet_ntop(AF_INET, &addr, str, ADDR_SIZE) != NULL);
}

/*
 * Get an IPv4 address and return as a string.  Return TRUE on
 * success, FALSE if a protocol or conversion botch.
 */
static int get_addr(DBusMessageIter *iter, const char *key,
    char local_addr[ADDR_SIZE])
{
	uint32_t value;

	if (_get_basic(iter, key, DBUS_TYPE_UINT32, &value) == TRUE &&
	    _cvt_ipv4(local_addr, value) == TRUE) {
		_DBG_DHCPCD("%s = %s (0x%x)", key, local_addr, value);
		return TRUE;
	} else
		return FALSE;
}

/*
 * D-Bus helper functions for parsing an array of basic values.
 */

static int _get_array_basic(DBusMessageIter *iter, const char *key,
    int basic_type, void *value, int *nelems, int max_elems)
{
	DBusMessageIter array, entry;
	int type;

	*nelems = 0;
	dbus_message_iter_recurse(iter, &array);
	type = dbus_message_iter_get_arg_type(&array);
	if (type != DBUS_TYPE_ARRAY) {
		(void) bad_type(key, DBUS_TYPE_ARRAY, type);
		return FALSE;
	}
	dbus_message_iter_recurse(&array, &entry);
	/* TODO(sleffler) verify dbus_type_is_fixed */
	type = dbus_message_iter_get_arg_type(&entry);
	if (type != basic_type) {
		(void) bad_type(key, basic_type, type);
		return FALSE;
	} else {
		dbus_message_iter_get_fixed_array(&entry, value, nelems);
		if ((*nelems) > max_elems)
			return bad_array_size(key, *nelems, max_elems);
		return TRUE;
	}
}
static int get_array_uint32(DBusMessageIter *iter, const char *key,
    uint32_t **value, int *nelems, int max_elems)
{
	int status = _get_array_basic(iter, key, DBUS_TYPE_UINT32,
	    value, nelems, max_elems);
	_DBG_DHCPCD("%s = [%d]{ 0x%x, 0x%x, 0x%x, 0x%x, 0x%x }", key, *nelems
	    , (*nelems > 0) ? (*value)[0] : -1
	    , (*nelems > 1) ? (*value)[1] : -1
	    , (*nelems > 2) ? (*value)[2] : -1
	    , (*nelems > 3) ? (*value)[3] : -1
	    , (*nelems > 4) ? (*value)[4] : -1
	);
	return status;
}

/*
 * Parse and return an array of strings.  The returned data are references
 * to the d-bus data and most be copied. The array size is returned in nelems.
 * The max_elems parameter specifies the maximum number of elements to accept;
 * if more elements are present in the d-bus data then FALSE is returned.
 * Otherwise TRUE is returned on success or FALSE if the wire format is
 * malformed.
 */
static int get_array_string(DBusMessageIter *iter, const char *key,
    char **value, int *nelems, int max_elems)
{
	DBusMessageIter array, entry;
	int type;

	*nelems = 0;
	dbus_message_iter_recurse(iter, &array);
	type = dbus_message_iter_get_arg_type(&array);
	if (type != DBUS_TYPE_ARRAY) {
		(void) bad_type(key, DBUS_TYPE_ARRAY, type);
		return FALSE;
	}
	type = dbus_message_iter_get_element_type(&array);
	if (type != DBUS_TYPE_STRING)
		return bad_type(key, DBUS_TYPE_STRING, type);
	dbus_message_iter_recurse(&array, &entry);
	while (dbus_message_iter_get_arg_type(&entry) != DBUS_TYPE_INVALID) {
		if ((*nelems) >= max_elems)
			return bad_array_size(key, (*nelems), max_elems);

		dbus_message_iter_get_basic(&entry, &value[*nelems]);
		_DBG_DHCPCD("%s = [%d]{ %s }", key, *nelems, value[*nelems]);
		(*nelems)++;

		dbus_message_iter_next(&entry);
	}
	return TRUE;
}

static DBusHandlerResult dhcpcd_filter(DBusConnection *conn,
    DBusMessage *msg, void *data)
{
#define	iseq(_a, _b) (g_ascii_strcasecmp((_a), (_b)) == 0)
	DBusMessageIter iter, dict;
	dbus_uint32_t pid;
	struct dhcpcd_task *task;
	const char *reason, *key;
	char *value;
	struct connman_ipaddress ipaddr;
	char local_addr[ADDR_SIZE];
	char bcast_addr[ADDR_SIZE];
	char gateway_addr[ADDR_SIZE];
	char name_servers[MAXDNS][ADDR_SIZE], *dns_servers[MAXDNS+1];
	char *search_domains[MAXDOMAINS+1];
	uint32_t *uint32_array;
	int i, nelems;
	dbus_bool_t is_event, is_status;

	is_event = dbus_message_is_signal(msg, DHCPCD_INTF, "Event");
	is_status = dbus_message_is_signal(msg, DHCPCD_INTF, "StatusChanged");

	/* Accept StatusChanged events long enough that we can get dbus addr */
	if (is_event == FALSE && is_status == FALSE)
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;

	dbus_message_iter_init(msg, &iter);

	/* PID of dhcpcd process */
	dbus_message_iter_get_basic(&iter, &pid);
	task = find_task_by_pid(pid);
	if (task == NULL) {
		connman_error("%s: no task for pid %d", __func__, pid);
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
	}

	/* Save the DBus address of the sender if we don't have it yet */
	if (task->dbus_address == NULL) {
		const char *message_dbus_address;
		message_dbus_address = dbus_message_get_sender(msg);
		task->dbus_address = g_strdup(message_dbus_address);
		_DBG_DHCPCD("pid %d has DBus address %s", pid, task->dbus_address);
	}

	/* Past here, only events are handled */
	if (is_event == FALSE)
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;

	dbus_message_iter_next(&iter);

	/* reason for message */
	dbus_message_iter_get_basic(&iter, &reason);
	dbus_message_iter_next(&iter);

	_DBG_DHCPCD("change %s state to %s",
	    connman_ipconfig_get_ifname(task->ipconfig), reason);

	memset(&ipaddr, 0, sizeof(ipaddr));
	ipaddr.af = AF_INET;
	ipaddr.mask |= CONNMAN_IPCONFIG_AF;

	dbus_message_iter_recurse(&iter, &dict);
	while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
		DBusMessageIter entry;
		int type;

		dbus_message_iter_recurse(&dict, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		type = dbus_message_iter_get_arg_type(&entry);
		if (type != DBUS_TYPE_VARIANT)
			return bad_type(key, DBUS_TYPE_VARIANT, type);

		if (iseq(key, "IPAddress")) {
			if (get_addr(&entry, key, local_addr) == FALSE)
				return DBUS_HANDLER_RESULT_HANDLED;
			ipaddr.local = local_addr;
			ipaddr.mask |= CONNMAN_IPCONFIG_LOCAL;
		} else if (iseq(key, "SubnetCIDR")) {
			uint8_t byte_value;

			if (get_byte(&entry, key, &byte_value) == FALSE)
				return DBUS_HANDLER_RESULT_HANDLED;
			ipaddr.prefixlen = byte_value;
			ipaddr.mask |= CONNMAN_IPCONFIG_PREFIX;
		} else if (iseq(key, "BroadcastAddress")) {
			if (get_addr(&entry, key, bcast_addr) == FALSE)
				return DBUS_HANDLER_RESULT_HANDLED;
			ipaddr.broadcast = bcast_addr;
			ipaddr.mask |= CONNMAN_IPCONFIG_BCAST;
		} else if (iseq(key, "Routers")) {
			if (get_array_uint32(&entry, key,
			    /* NB: 1000 is just "big" */
			    &uint32_array, &nelems, 1000) == FALSE)
				return DBUS_HANDLER_RESULT_HANDLED;
			/* NB: silently discard extra routers */
			if (nelems < 1)
				return bad_array_size(key, nelems, 1);
			/* TODO(sleffler) check return */
			(void) _cvt_ipv4(gateway_addr, uint32_array[0]);
			ipaddr.gateway = gateway_addr;
			ipaddr.mask |= CONNMAN_IPCONFIG_GW;
		} else if (iseq(key, "DomainNameServers")) {
			if (get_array_uint32(&entry, key,
			    &uint32_array, &nelems, 10) == FALSE)
				return DBUS_HANDLER_RESULT_HANDLED;
			for (i = 0; i < nelems; i++) {
				dns_servers[i] = &name_servers[i][0];
				_cvt_ipv4(dns_servers[i], uint32_array[i]);
			}
			dns_servers[i] = NULL;	/* NB: for g_strdupv */
			ipaddr.dns_servers = dns_servers;
			ipaddr.mask |= CONNMAN_IPCONFIG_DNS;
		} else if (iseq(key, "DomainName")) {
			if (get_string(&entry, key, &value) == FALSE)
				return DBUS_HANDLER_RESULT_HANDLED;
			ipaddr.domain_name = value;
			ipaddr.mask |= CONNMAN_IPCONFIG_DOMAIN;
		} else if (iseq(key, "DomainSearch")) {
			if (get_array_string(&entry, key,
			    search_domains, &nelems, 10) == FALSE)
				return DBUS_HANDLER_RESULT_HANDLED;
			search_domains[nelems] = NULL;	/* NB: for g_strdupv */
			ipaddr.search_domains = search_domains;
			ipaddr.mask |= CONNMAN_IPCONFIG_SEARCH;
		} else if (iseq(key, "Hostname")) {
			if (get_string(&entry, key, &value) == FALSE)
				return DBUS_HANDLER_RESULT_HANDLED;
			/* TODO(sleffler) add support */
		} else if (iseq(key, "InterfaceMTU")) {
			uint16_t uint16_value;

			if (get_uint16(&entry, key, &uint16_value) == FALSE)
				return DBUS_HANDLER_RESULT_HANDLED;
			/* NB: restrict min MTU per dhcpcd-script */
			if (576 <= uint16_value) {
				ipaddr.mtu = uint16_value;
				ipaddr.mask |= CONNMAN_IPCONFIG_MTU;
			} else
				_DBG_DHCPCD("%s = %u (skipped)", key,
				     uint16_value);
		} else
			_DBG_DHCPCD("ignore key %s", key);

		dbus_message_iter_next(&dict);
	}

	if (g_ascii_strcasecmp(reason, "BOUND") == 0
	    || g_ascii_strcasecmp(reason, "REBOOT") == 0
	    /*
	     * Even if IP state hasn't changed, this is the path to
	     * bringing the service from CONFIGURATION state to READY.
	     */
	    || g_ascii_strcasecmp(reason, "RENEW") == 0
	    || g_ascii_strcasecmp(reason, "REBIND") == 0
	) {
		connman_ipconfig_bind(task->ipconfig, &ipaddr);
	} else if (g_ascii_strcasecmp(reason, "FAIL") == 0) {
		connman_ipconfig_set_error(task->ipconfig,
		    CONNMAN_ELEMENT_ERROR_DHCP_FAILED);
	}
	return DBUS_HANDLER_RESULT_HANDLED;
#undef iseq
}

static const char *dhcpcd_rule = "path=" DHCPCD_PATH
    ",interface=" DHCPCD_INTF;

static int dhcpcd_init(void)
{
	int err;

	/* TODO(sleffler) check return values */
	connection = connman_dbus_get_connection();
	dbus_connection_add_filter(connection, dhcpcd_filter, NULL, NULL);
	dbus_bus_add_match(connection, dhcpcd_rule, NULL);

	err = connman_ipconfig_driver_register(&dhcpcd_driver);
	if (err < 0)
		dbus_connection_unref(connection);
	return err;
}

static void dhcpcd_exit(void)
{
	GSList *list;

	for (list = task_list; list; list = list->next) {
		struct dhcpcd_task *task = list->data;

		_DBG_DHCPCD("kill process %d", task->pid);

		kill_task(task);
		unlink_task(task);
	}
	g_slist_free(task_list);

	connman_ipconfig_driver_unregister(&dhcpcd_driver);

	dbus_bus_remove_match(connection, dhcpcd_rule, NULL);
	dbus_connection_remove_filter(connection, dhcpcd_filter, NULL);
	dbus_connection_unref(connection);
}

CONNMAN_PLUGIN_DEFINE(dhcpcd, "Chrome OS DHCP client", VERSION,
    CONNMAN_PLUGIN_PRIORITY_DEFAULT, dhcpcd_init, dhcpcd_exit)
